﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Bouyei.GeoCore.GeoParsers.Dbf
{
    public class DbfReader : BaseFile
    {
        public DbfReader(string dbfFile, Encoding encoding=null)
            : base(dbfFile, encoding)
        { }
        public override void Dispose()
        {
            base.Dispose();
        }

        public DataTable Reader()
        {
            using (var fileReader = new BinaryReader(File.OpenRead(PathName)))
            {
                if (encoding == null)
                    encoding = PredictEncoding(fileReader);

                byte[] buffer = fileReader.ReadBytes(Marshal.SizeOf(typeof(DBFHeader)));

                // Marshall the header into a DBFHeader structure
                GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                DBFHeader header = (DBFHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(DBFHeader));
                handle.Free();

                // Read in all the field descriptors. Per the spec, 13 (0D) marks the end of the field descriptors
                List<FieldDescriptor> fields = new List<FieldDescriptor>(8);
                int fieldSize = Marshal.SizeOf(typeof(FieldDescriptor));

                while ((13 != fileReader.PeekChar()))
                {
                    //fileReader.BaseStream.Position = fileReader.BaseStream.Position - 1;

                    buffer = fileReader.ReadBytes(fieldSize);
                    handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                    fields.Add((FieldDescriptor)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(FieldDescriptor)));
                    handle.Free();
                }

                DataTable table = new DataTable();
                //fill  columns to table
                var columns = ToTableColumn(fields);
                table.Columns.AddRange(columns);

                // Read in the first row of records, we need this to help determine column types below
                fileReader.BaseStream.Seek(header.headerLen + 1, SeekOrigin.Begin);
                buffer = fileReader.ReadBytes(header.recordLen);

                using (var blockReader = new BinaryReader(new MemoryStream(buffer)))
                {
                    // Skip past the end of the header.
                    fileReader.BaseStream.Seek(header.headerLen, SeekOrigin.Begin);

                    //fill rows to table
                    StreamRowReader(header, fileReader, fields, (row) =>
                    {
                        table.Rows.Add(row.ToArray());
                        return true;
                    });
                    return table;
                }
            }
        }

        public void StreamReader(Func<DataColumn[], List<object>, bool> fun)
        {
            using (var fileReader = new BinaryReader(File.OpenRead(PathName)))
            {
                if (encoding == null)
                    encoding = PredictEncoding(fileReader);

                byte[] buffer = fileReader.ReadBytes(Marshal.SizeOf(typeof(DBFHeader)));

                // Marshall the header into a DBFHeader structure
                GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                DBFHeader header = (DBFHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(DBFHeader));
                handle.Free();

                // Read in all the field descriptors. Per the spec, 13 (0D) marks the end of the field descriptors
                List<FieldDescriptor> fields = new List<FieldDescriptor>(8);
                int fieldSize = Marshal.SizeOf(typeof(FieldDescriptor));

                while ((13 != fileReader.PeekChar()))
                {
                    //fileReader.BaseStream.Position = fileReader.BaseStream.Position - 1;

                    buffer = fileReader.ReadBytes(fieldSize);
                    handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                    fields.Add((FieldDescriptor)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(FieldDescriptor)));
                    handle.Free();
                }

                var columns = ToTableColumn(fields);

                // Read in the first row of records, we need this to help determine column types below
                fileReader.BaseStream.Seek(header.headerLen + 1, SeekOrigin.Begin);
                buffer = fileReader.ReadBytes(header.recordLen);

                using (var blockReader = new BinaryReader(new MemoryStream(buffer)))
                {
                    // Skip past the end of the header.
                    fileReader.BaseStream.Seek(header.headerLen, SeekOrigin.Begin);

                    //fill rows to table
                    StreamRowReader(header, fileReader, fields, (row) =>
                    {
                        return fun?.Invoke(columns, row) ?? false;
                    });
                }
            }
        }

        public List<DataColumn> ReadColumns()
        {
            List<DataColumn> columns = new List<DataColumn>(8);

            using (var fileReader = new BinaryReader(File.OpenRead(PathName)))
            {
                if (encoding == null)
                    encoding = PredictEncoding(fileReader);

                byte[] buffer = fileReader.ReadBytes(Marshal.SizeOf(typeof(DBFHeader)));

                // Marshall the header into a DBFHeader structure
                GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                DBFHeader header = (DBFHeader)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(DBFHeader));
                handle.Free();

                int fieldSize = Marshal.SizeOf(typeof(FieldDescriptor));
                while ((13 != fileReader.PeekChar()))
                {
                    //fileReader.BaseStream.Position = fileReader.BaseStream.Position - 1;

                    buffer = fileReader.ReadBytes(fieldSize);
                    handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
                    FieldDescriptor fDes = (FieldDescriptor)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(FieldDescriptor));
                    var col = FieldDescriptorToDataColumn(fDes);
                    columns.Add(col);
                    handle.Free();
                }
            }
            return columns;
        }

        private DataColumn[] ToTableColumn(List<FieldDescriptor> fields)
        {
            // Create the columns in our new DataTable
            DataColumn[] columns = new DataColumn[fields.Count];
            int idx = 0;
            foreach (FieldDescriptor field in fields)
            {
                DataColumn col = FieldDescriptorToDataColumn(field);
                columns[idx++] = col;
            }
            return columns;
        }

        private DataColumn FieldDescriptorToDataColumn(FieldDescriptor field)
        {
            DataColumn col = new DataColumn();
            switch (field.fieldType)
            {
                case 'N':
                    if (field.count == 0)
                    {
                        if (field.fieldLen <= 10 && field.fieldLen > 6)
                        {
                            col = new DataColumn(field.fieldName, typeof(int));
                        }
                        else if (field.fieldLen <= 6)
                        {
                            col = new DataColumn(field.fieldName, typeof(short));
                        }
                        else
                        {
                            col = new DataColumn(field.fieldName, typeof(long));
                        }
                    }
                    else
                    {
                        if (field.fieldLen <= 6)
                        {
                            col = new DataColumn(field.fieldName, typeof(float));
                        }
                        else
                        {
                            col = new DataColumn(field.fieldName, typeof(double));
                        }
                    }
                    break;
                case 'C':
                    col = new DataColumn(field.fieldName, typeof(string));
                    break;
                case 'T':
                    // You can uncomment this to see the time component in the grid
                    //col = new DataColumn(field.fieldName, typeof(string));
                    col = new DataColumn(field.fieldName, typeof(DateTime));
                    break;
                case 'D':
                    col = new DataColumn(field.fieldName, typeof(DateTime));
                    break;
                case 'L':
                    col = new DataColumn(field.fieldName, typeof(bool));
                    break;
                case 'F':
                    col = new DataColumn(field.fieldName, typeof(float));
                    break;
                case 'I':
                    col = new DataColumn(field.fieldName, typeof(int));
                    break;
                case 'B':
                    col = new DataColumn(field.fieldName, typeof(byte[]));
                    break;
                default:
                    col = new DataColumn(field.fieldName, typeof(string));
                    break;
            }
            return col;
        }
        private void StreamRowReader(DBFHeader header, BinaryReader fileReader
              , List<FieldDescriptor> fields, Func<List<object>, bool> fun)
        {
            byte[] buffer = null;

            string year;
            string month;
            string day;
            long lDate;
            long lTime;
            using (BinaryReader rowReader = new BinaryReader(new MemoryStream()))
            {
                // Read in all the records
                for (int counter = 0; counter <= header.numRecords - 1; counter++)
                {
                    // First we'll read the entire record into a buffer and then read each field from the buffer
                    // This helps account for any extra space at the end of each record and probably performs better
                    buffer = fileReader.ReadBytes(header.recordLen);
                    rowReader.BaseStream.Seek(0, SeekOrigin.Begin);
                    rowReader.BaseStream.Write(buffer, 0, buffer.Length);
                    rowReader.BaseStream.Seek(0, SeekOrigin.Begin);
                    List<object> row = new List<object>(header.numRecords);

                    if (rowReader.ReadChar() == '*')
                    {
                        continue;
                    }

                    foreach (FieldDescriptor field in fields)
                    {
                        switch (field.fieldType)
                        {
                            case 'N':  // Number
                                {
                                    var array = rowReader.ReadChars(field.fieldLen);
                                    if (IsInter(array))
                                    {
                                        int result = 0;
                                        int.TryParse(new string(array), out result);
                                        row.Add(result);
                                    }
                                    else
                                    {
                                        double result = 0;
                                        double.TryParse(new string(array), out result);
                                        row.Add(result);
                                    }
                                }
                                break;
                            case 'C': // String
                                {
                                    if (encoding == null)
                                        row.Add(rowReader.ReadChars(field.fieldLen));
                                    else
                                        row.Add(encoding.GetString(rowReader.ReadBytes(field.fieldLen)).Trim());
                                }
                                break;
                            case 'D': // Date (YYYYMMDD)
                                {
                                    year = encoding.GetString(rowReader.ReadBytes(4));
                                    month = encoding.GetString(rowReader.ReadBytes(2));
                                    day = encoding.GetString(rowReader.ReadBytes(2));
                                    DateTime val = DateTime.MinValue;
                                    try
                                    {
                                        val = new DateTime(Int32.Parse(year), Int32.Parse(month), Int32.Parse(day));
                                    }
                                    catch
                                    { }
                                    row.Add(val);
                                }
                                break;
                            case 'T': // Timestamp, 8 bytes - two integers, first for date, second for time
                                      // Date is the number of days since 01/01/4713 BC (Julian Days)
                                      // Time is hours * 3600000L + minutes * 60000L + Seconds * 1000L (Milliseconds since midnight)
                                {
                                    lDate = rowReader.ReadInt32();
                                    lTime = rowReader.ReadInt32() * 10000L;
                                    row.Add(JulianToDateTime(lDate).AddTicks(lTime));
                                }
                                break;
                            case 'L': // Boolean (Y/N)
                                {
                                    row.Add('Y' == rowReader.ReadByte());
                                }
                                break;
                            case 'F':
                                {
                                    byte[] val = rowReader.ReadBytes(field.fieldLen);
                                    double result = 0;
                                    double.TryParse(encoding.GetString(val), out result);
                                    row.Add(result);
                                }
                                break;
                            case 'B':
                                {
                                    var bytes = rowReader.ReadBytes(field.fieldLen);
                                    row.Add(bytes);
                                }
                                break;
                        }
                    }

                    if (!fun?.Invoke(row) ?? false) return;
                }
            }
        }

        private bool IsNumber(byte[] values)
        {
            unsafe
            {
                int number_count = 0;
                int point_count = 0;
                int space_count = 0;

                fixed (byte* s = values)
                {
                    for (int i = 0; i < values.Length; ++i)
                    {
                        byte b = *(s + i);
                        if ((b >= 48 && b <= 57))
                        {
                            number_count += 1;
                        }
                        else if (b == 46)
                        {
                            point_count += 1;
                        }
                        else if (b == 32)
                        {
                            space_count += 1;
                        }
                        else
                        {
                            if (!(b == 101 && (b + 1) == 43))//科学计数法问题
                            {
                                return false;
                            }
                        }
                    }
                }
                return (number_count > 0 && point_count < 2);
            }
        }

        private bool IsInter(byte[] values)
        {
            unsafe
            {
                fixed (byte* s = values)
                    for (int i = 0; i < values.Length; ++i)
                    {
                        byte b = *(s + i);

                        if (b == 46)
                        {
                            return false;
                        }
                    }
            }
            //BitConverter.GetBytes(1.4);
            return true;
        }
        private bool IsInter(char[] values)
        {
            unsafe
            {
                fixed (char* s = values)
                    for (int i = 0; i < values.Length; ++i)
                    {
                        var b = *(s + i);

                        if (b == '.')
                        {
                            return false;
                        }
                    }
            }
            //BitConverter.GetBytes(1.4);
            return true;
        }
        private DateTime JulianToDateTime(long lJDN)
        {
            double p = Convert.ToDouble(lJDN);
            double s1 = p + 68569;
            double n = Math.Floor(4 * s1 / 146097);
            double s2 = s1 - Math.Floor((146097 * n + 3) / 4);
            double i = Math.Floor(4000 * (s2 + 1) / 1461001);
            double s3 = s2 - Math.Floor(1461 * i / 4) + 31;
            double q = Math.Floor(80 * s3 / 2447);
            double d = s3 - Math.Floor(2447 * q / 80);
            double s4 = Math.Floor(q / 11);
            double m = q + 2 - 12 * s4;
            double j = 100 * (n - 49) + i + s4;
            return new DateTime(Convert.ToInt32(j), Convert.ToInt32(m), Convert.ToInt32(d));
        }

        private Encoding PredictEncoding(BinaryReader br)
        {
            var buffer = br.ReadBytes(3);

            br.BaseStream.Seek(0, SeekOrigin.Begin);

            if (buffer[0] < 0xEF)
            {
                return Encoding.Default;
            }
            if (buffer[0] == 0xEF && buffer[1] == 0xBB)
            {
                return Encoding.UTF8;
            }
            if (buffer[0] == 0xFF && buffer[1] == 0xFE)//FF FE 255 254  UTF-16 LE (little-endian)
            {
                return Encoding.Unicode;
            }
            if (buffer[0] == 0xFE && buffer[1] == 0xFF)//FE FF 254 255  UTF-16 BE (big-endian)
            {
                return Encoding.BigEndianUnicode;
            }
            return Encoding.GetEncoding("GBK");//GBK
        }
    }
}
